package os_listener;


import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKind.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKind.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKind.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKind.OVERFLOW;

import java.beans.XMLDecoder;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;

import GUI.Encoder;


/**
 * this class listen to the dropbox directory and respond to changes
 */
public class OS_Listener implements Runnable
{
	//will be used for keeping the massages from OS
	String massage;
	//flag for updating the state of the service
	Boolean stop;
	// the watch service
	WatchService watcher;
	Encoder e;
    private final Map<WatchKey,Path> keys;
    private boolean trace = false;
    private final boolean recursive;
    FileBytes fb;
    // true if the change spotted should not be taken care of
    boolean shared_folder;
	
    @SuppressWarnings("unchecked")
    static <T> WatchEvent<T> cast(WatchEvent<?> event) 
    {
        return (WatchEvent<T>)event;
    }
    
    /**
     * Register the given directory with the WatchService
     */
    private void register(Path dir) throws IOException 
    {
        WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
        if (trace) 
        {
			Path prev = keys.get(key);
			if (prev == null)
			{
				System.out.format("register: %s\n", dir);
			} else {
				if (!dir.equals(prev)) 
				{
					System.out.format("update: %s -> %s\n", prev, dir);
				}
			}
		}
        keys.put(key, dir);
    }
    
    /**
     * Register the given directory, and all its sub-directories, with the
     * WatchService.
     */
    private void registerAll(final Path start) throws IOException 
    {
    	// register directory and sub-directories
    	Files.walkFileTree(start, new SimpleFileVisitor<Path>() 
    			{
    		@Override
    		public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
    		throws IOException
    		{
    			register(dir);
    			return FileVisitResult.CONTINUE;
    		}
    			});
    }
	
	/**
	 * A handle to the unique OS_Listener instance.
	 */
	static private OS_Listener instance = null;
	//for the singleton
	private static Object someObject=new Object();
	
	// the private constructor of this class
	private OS_Listener() throws FileNotFoundException 
	{
		// Initializing the mmebers of this class
		massage=null;
		stop=true;
		shared_folder = false;
		this.keys = new HashMap<WatchKey,Path>();
		this.recursive = true;
		
		XMLDecoder d = new XMLDecoder(new BufferedInputStream(new FileInputStream("C:/Program Files/m&l_dropbox/details.xml")));
		// getting the user information
		e = (Encoder) d.readObject();
		d.close();
		
		// creating an instance of the fileBytes class
		fb = new FileBytes(e.getUserName(), e.getPasswword());
		// getting the dropbox path
		Path dir = Paths.get(e.getPath() + "\\DropBox");
		try 
		{
			// create the watcher
			watcher = FileSystems.getDefault().newWatchService();
			registerAll(dir);
		} 
		catch (IOException e) {}
	}
	
	 /**
	 * @return The unique instance of this class.
	 * @throws FileNotFoundException 
	 */
	static public OS_Listener getInstance() throws FileNotFoundException 
	{
		if(null == instance) 
		{
			//to insure that the time slice doesn't cause the creation of 2 instances
			synchronized (someObject) 
			{
				if (null == instance)
					instance = new OS_Listener();
			}
		}
	    return instance;
	}
	
	/**
	 * the Thread run function that execute this class's work
	 */
	public void run() 
	{
		// while the service is open keep running
		while(stop==false)
		{
            // wait for key to be signalled
            WatchKey key;
            try 
            {
            	// get the key  (new events)
                key = watcher.take();
            }
            // the case that the service was stopped or something went wrong
            catch (Exception x) 
            {
                return;
            }

            Path dir = keys.get(key);
            // the case that the watch key was not recognized
            if (dir == null) 
            {
                System.err.println("WatchKey not recognized!!");
                continue;
            }
            
            // for each event
			for (WatchEvent<?> event: key.pollEvents()) 
			{
				WatchEvent.Kind kind = event.kind();

				// TBD - provide example of how OVERFLOW event is handled
				if (kind == OVERFLOW) 
				{
					continue;
				}

				// Context for directory entry event is the file name of entry
				WatchEvent<Path> ev = cast(event);
				String name = ev.context().toString();
				Path child = dir.resolve(name);

				// the case that a directory or file was created
				if (recursive && kind == ENTRY_CREATE)
				{
					try 
					{
						// the case that this is a new directory
						if (Files.isDirectory(child, NOFOLLOW_LINKS)) 
						{
							// if directory is created, and watching recursively, then
							// register it and its sub-directories
							registerAll(child);
							// the case that this update should be taken care of
							if(!shared_folder)
							{
								// enter the directory into the server DB
								updateDir(name, true);
							}
							// the case that this update should not be taken care of
							else
							{
								shared_folder = false;
							}
						}
						else
						{
							// the case that this update should be taken care of
							if(!shared_folder)
							{
								processMassage(kind.toString(), name, child.toString());
							}
							// the case that this update should not be taken care of
							else
							{
								shared_folder = false;
							}
						}
					}
					catch (IOException x){} catch (Exception e) {}
				}
				// the case that a directory or file was not created
				else
				{
					// the case that a directory or file was deleted
					if(recursive && kind == ENTRY_DELETE)
					{
						try 
						{
							// the case that this update should be taken care of
							if(!shared_folder)
							{
								// delete directory if directory
								updateDir(name, false);
								// delete file if file
								processMassage(kind.toString(), name, child.toString());
								// delete tag of files from the directory if directory
								deleteTagsFilesFromDir();
							}
							// the case that this update should not be taken care of
							else
							{
								shared_folder = false;
							}
						}
						catch (IOException x){} catch (Exception e) {}
					}
					// the case that a directory or file was not deleted
					else
					{
						// the case that this is not a directory
						if(!Files.isDirectory(child, NOFOLLOW_LINKS))
						{
							try
							{
								// entry modify for files
								processMassage(kind.toString(), name, child.toString());
							}
							catch (Exception e) {}
						}
					}
				}
			}
			
            // reset key and remove from set if directory no longer accessible
            boolean valid = key.reset();
            if (!valid)
            {
                keys.remove(key);

                // all directories are inaccessible
                if (keys.isEmpty())
                {
                    break;
                }
            }
		}
	}

	/*
	 * this function delete the tags of files from deleted directory
	 */
	private void deleteTagsFilesFromDir() throws IOException 
	{
		Socket socket=null;
		PrintWriter out=null;
		String tagHost = "localhost";
		String validation;
		try 
		{
			socket = new Socket(tagHost,4444);
			out = new PrintWriter(socket.getOutputStream(),true);
			BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			// sending the information to the tag program
			out.println("delete - dir");
			out.println("");
			out.println("");
			// waiting for a validation message
			validation = in.readLine();
		} 
		catch (UnknownHostException e) 
		{
			System.err.println("Don't know host: "+tagHost);
		} 
		catch (IOException e) 
		{
			System.err.println("Cannot get IO for connection to "+tagHost);
		}
		finally 
		{
			// close connection (Order important)
			if (out != null)
				out.close();  
			if (socket!= null) 
				socket.close();
		}
	}

	/*
	 * this function handles the different cases of file changes in the dropbox
	 * @param kind - the type of the change
	 * @param fileName - the file that changed name
	 * @param child - the file that changed path
	 * @throws Exception
	 */
	private void processMassage(String kind, String fileName, String child) throws Exception 
	{
		// the case that a new file was created
    	if(kind.equals("ENTRY_CREATE"))
    	{
    		Socket socket=null;
    		PrintWriter out=null;
    		//String serverHost = "localhost";// or IP address
    		// this is an example in a comment about how you can enter an ip address
    		String serverHost = "10.0.0.1";
    		String tagHost = "localhost";
    		String validation;
    		try
    		{
    			socket = new Socket(tagHost,4444);
    			out = new PrintWriter(socket.getOutputStream(),true);
    			BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    			// sending message for the tag manager program
    			out.println("create");
    			out.println(child);
    			out.println(fileName);
    			// waiting for a validation message 
    			validation = in.readLine();
    		} 
    		catch (UnknownHostException e) 
    		{
    			System.err.println("Dont know host: "+tagHost);
    		} 
    		catch (IOException e) 
    		{
    			System.err.println("Cannot get IO for connection to "+tagHost);
    		}
    		finally 
    		{
    			// close connection (Order important)
    			if (out != null)
    				out.close();  
    			if (socket!= null) 
    				socket.close();
    		}

    		File f = new File(child.toString());
    		byte[] bytes;
    		String hash, response;
    		bytes = convFileToByte(f);
    		hash = getHash(bytes);
    		// updating the fileBytes instance
    		fb.setFileName(fileName);
    		fb.setPath(child.toString());
    		fb.setBytes(null);
    		fb.setCreated(true);
    		fb.setHash(hash);
    		
    		Socket socket2 = new Socket(serverHost, 5555);
    		ObjectOutputStream out2 = new ObjectOutputStream(socket2.getOutputStream());
    		BufferedReader in2 = new BufferedReader(new InputStreamReader(socket2.getInputStream()));
    		// sending the update to the server
    		out2.writeObject(fb);
    		// waiting for a response
    		response = in2.readLine();
    		// the case that the file needs to be uploaded to the server because it doesn't exist in it
    		if(response.equals("not exist"))
    		{
    			fb.setBytes(bytes);
    			// sending the update to the server
    			ObjectOutputStream out3 = new ObjectOutputStream(socket2.getOutputStream());
        		out3.writeObject(fb);
    		}
    	}
    	
    	// the case that a file was deleted
    	if(kind.equals("ENTRY_DELETE"))
    	{
    		Socket socket=null;
    		PrintWriter out=null;
    		//String serverHost = "localhost";// or IP address
    		// this is an example in a comment about how you can enter an ip address
    		String serverHost = "10.0.0.1";
    		String tagHost = "localhost";
    		String validation;
    		try 
    		{
    			socket = new Socket(tagHost,4444);
    			out = new PrintWriter(socket.getOutputStream(),true);
    			BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    			// sending message for the tag manager program
    			out.println("delete");
    			out.println(child);
    			out.println(fileName);
    			// waiting for a validation message 
    			validation = in.readLine();
    		} 
    		catch (UnknownHostException e) 
    		{
    			System.err.println("Don't know host: "+tagHost);
    		} 
    		catch (IOException e) 
    		{
    			System.err.println("Cannot get IO for connection to "+tagHost);
    		}
    		finally 
    		{
    			// close connection (Order important)
    			if (out != null)
    				out.close();  
    			if (socket!= null) 
    				socket.close();
    		}

    		// updating the fileBytes instance
      		fb.setFileName(fileName);
    		fb.setPath(child.toString());
    		fb.setBytes(null);
    		fb.setHash("");
    		fb.setCreated(false);
    		
    		Socket socket2 = new Socket(serverHost, 5555);
    		ObjectOutputStream out2 = new ObjectOutputStream(socket2.getOutputStream());
    		// sending the update to the server
    		out2.writeObject(fb);
    	}
    	
    	// the case that a file changed
    	if(kind.equals("ENTRY_MODIFY"))
    	{
    		processMassage("ENTRY_DELETE", fileName, child);
    		processMassage("ENTRY_CREATE", fileName, child);
    	}
	}
	
	/*
	 * this function handles the creation or delete of a directory
	 * @param dirName
	 * @param create - true if the directory was created, false otherwise
	 * @throws IOException
	 */
	private void updateDir(String dirName, boolean create) throws IOException
	{
		Socket socket=null;
		PrintWriter out=null;
		//String serverHost = "localhost";// or IP address
		// this is an example in a comment about how you can enter an ip address
		String serverHost = "10.0.0.1";
		try
		{
			socket = new Socket(serverHost,4567);
			out = new PrintWriter(socket.getOutputStream(),true);
			BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			// sending message for the server
			out.println(dirName);
			out.println(e.getUserName());
			// the case that the directory was created
			if(create)
			{
				out.println("create");
			}
			// the case that the directory was deleted
			else
			{
				out.println("delete");
			}
			// waiting for a validation message
			String validation = in.readLine();
			// updating the files' directory
			insertFilesInDir(e.getPath() + "\\DropBox\\" + dirName);
			
		} 
		catch (UnknownHostException e) 
		{
			System.err.println("Dont know host: "+serverHost);
		} 
		catch (IOException e) 
		{
			System.err.println("Cannot get IO for connection to "+serverHost);
		} 
		catch (Exception e) {} 
		finally 
		{
			// close connection (Order important)
			if (out != null)
				out.close();  
			if (socket!= null) 
				socket.close();
		}
	}
	
	/**
	 * insert the files in the directory 'dir_path' to DB files (by connecting to the server) if the
	 * directory isn't empty
	 * @param dir_path - the new directory
	 * @throws Exception 
	 */
	public void insertFilesInDir(String dir_path) throws Exception
	{
		File f = new File(dir_path);	
		String[] list = f.list();
		String fileName;
		//the case there are files in the directory
		if (list != null)
		{
			//run over the files in the directory 
			for (int i = 0; i < list.length; i++) 
			{
				fileName = list[i];
				// update the new file in the directory
				processMassage("ENTRY_CREATE" , fileName, dir_path + "\\" + fileName);
			}	
		}
	}
	
	/**
	 * this function tag/untag new files/deleted files 
	 * @param create - true if file was created, false otherwise
	 * @param fileName - the new file name
	 * @param child - the new file path
	 * @throws Exception
	 */
	public void tagSharedFiles(boolean create, String fileName, String child) throws Exception 
	{
		// the case that a file was created and needs to be tagged
    	if(create)
    	{
    		Socket socket=null;
    		PrintWriter out=null;
    		String tagHost = "localhost";
    		String validation;
    		try
    		{
    			socket = new Socket(tagHost,4444);
    			out = new PrintWriter(socket.getOutputStream(),true);
    			BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    			// sending message for the tag manager program
    			out.println("create");
    			out.println(child);
    			out.println(fileName);
    			// waiting for a validation message
    			validation = in.readLine();
    		} 
    		catch (UnknownHostException e) 
    		{
    			System.err.println("Dont know host: "+tagHost);
    		} 
    		catch (IOException e) 
    		{
    			System.err.println("Cannot get IO for connection to "+tagHost);
    		}
    		finally 
    		{
    			// close connection (Order important)
    			if (out != null)
    				out.close();  
    			if (socket!= null) 
    				socket.close();
    		}
    	}
    	// the case that the file was deleted
    	else
    	{
    		Socket socket=null;
    		PrintWriter out=null;
    		String tagHost = "localhost";
    		String validation;
    		try 
    		{
    			socket = new Socket(tagHost,4444);
    			out = new PrintWriter(socket.getOutputStream(),true);
    			BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    			// sending message for the tag manager program
    			out.println("delete");
    			out.println(child);
    			out.println(fileName);
    			// waiting for a validation message
    			validation = in.readLine();
    		} 
    		catch (UnknownHostException e) 
    		{
    			System.err.println("Don't know host: "+tagHost);
    		} 
    		catch (IOException e) 
    		{
    			System.err.println("Cannot get IO for connection to "+tagHost);
    		}
    		finally 
    		{
    			// close connection (Order important)
    			if (out != null)
    				out.close();  
    			if (socket!= null) 
    				socket.close();
    		}
    	}
	}
	
	//getters and setters of this class
	
	public void setShared_folder(boolean shared_folder) 
	{
		this.shared_folder = shared_folder;
	}
	
	public Boolean getStop() 
	{
		return stop;
	}

	public void setStop(Boolean stop) throws IOException 
	{
		this.stop = stop;
		// the case that the service needs to be stopped
		if(stop == true)
		{
			watcher.close();
		}
	}

	// convert a file to an array of bytes - taken from the web
	public byte[] convFileToByte(File file)
	{
		FileInputStream fis = null;
		try 
		{
			fis = new FileInputStream(file);
		} 
		catch (FileNotFoundException e1) 
		{
			e1.printStackTrace();
		}
		//InputStream in = resource.openStream();
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		byte[] buf = new byte[1024];
		try 
		{
			for (int readNum; (readNum = fis.read(buf)) != -1;) 
			{
				bos.write(buf, 0, readNum); //no doubt here is 0
				//Writes len bytes from the specified byte array starting at offset off to this byte array output stream.
				// System.out.println("read " + readNum + " bytes,");
			}
		} 
		catch (IOException ex)
		{
			// Logger.getLogger(genJpeg.class.getName()).log(Level.SEVERE, null, ex);
		}
		byte[] bytes = bos.toByteArray();

		// System.out.println("bytes.hashCode() = "+hashCode);
		try 
		{
			fis.close();
			bos.flush();
			bos.close();
		} 
		catch (IOException e) 
		{
			e.printStackTrace();
		}
		return bytes;
	}
	
	/*
	 * this function gets an array of bytes (file) and return the hash string of this file   
	 */
	private String getHash(byte[] bytes) throws NoSuchAlgorithmException
	{
		MessageDigest m = MessageDigest.getInstance("MD5");
		m.update(bytes, 0, bytes.length);
		String hash = new BigInteger(1,m.digest()).toString(16);
		return hash;
	}
}




